Tela de login: credencial única, sem banco
Toda rota passa por um before_request que redireciona para /login quem não tem sessão — inclusive os POSTs, então /exportar e /salvar-sd sem cookie não chegam ao handler. Ficam de fora só a própria entrada e /static (a tela de login precisa da folha de estilo). Credencial única em constante (admin@iasis.com.br / admin123), comparada com compare_digest nos dois campos e com a mesma mensagem para usuário errado e senha errada — dizer qual dos dois falhou entrega metade da credencial. Sem banco, sem cadastro e sem recuperação de senha: a app é ferramenta interna de uso individual, e isto existe para o upload e o writeback não ficarem abertos a quem alcançar a porta quando ela roda hospedada. SECRET_KEY não tem default assado na imagem — chave fixa numa imagem vale para toda cópia dela. Sem a variável, o servidor sorteia uma por processo e o login cai a cada restart, que é a mesma vida útil que a sessão da SD já tem; é seguro porque o gunicorn roda com -w 1. APP_USUARIO/APP_SENHA seguem a convenção do Dockerfile: default lá, compose só espelha. O Sair encerra a sessão do navegador sem descartar a SD em memória — é a mesma máquina, o mesmo processo. E o teste_e2e faz login uma vez no test client, senão todo request dele voltaria 302. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+89
-1
@@ -25,18 +25,28 @@ botão nenhum.
|
||||
Estado em memória de processo, uma SD por vez: é ferramenta local de uso
|
||||
individual. Reiniciar o servidor descarta a sessão — o SD.md de origem continua
|
||||
intacto no disco, e nada é gravado sem ação explícita (Salvar SD.md).
|
||||
|
||||
Na frente de tudo isso há uma tela de login com credencial única, fixa na
|
||||
configuração — sem banco, sem cadastro, sem recuperação de senha. Ela não
|
||||
divide dados entre pessoas (a sessão da SD continua sendo uma só, do processo);
|
||||
serve para que upload, writeback e download não fiquem abertos a quem alcançar
|
||||
a porta quando a app roda hospedada.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hmac
|
||||
import io
|
||||
import os
|
||||
import secrets
|
||||
import sys
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "scripts"))
|
||||
|
||||
from flask import Flask, redirect, render_template, request, send_file, url_for
|
||||
from flask import (Flask, redirect, render_template, request, send_file, session,
|
||||
url_for)
|
||||
|
||||
import escrita_sd
|
||||
import sessao as S
|
||||
@@ -59,6 +69,84 @@ app.jinja_env.globals["dias"] = lambda a, b: (date.fromisoformat(b) - date.fromi
|
||||
ATUAL: dict = {"sessao": None, "aviso": None, "erro": None}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Porta de entrada
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Credencial única, fixa no código. Não existe banco, cadastro nem recuperação
|
||||
# de senha: a app é ferramenta interna de uso individual, e a autenticação aqui
|
||||
# serve para não deixar o upload e o Salvar SD.md abertos a quem alcançar a
|
||||
# porta. As variáveis de ambiente existem só para trocar a credencial no deploy
|
||||
# sem editar código — os defaults abaixo são a credencial acordada.
|
||||
USUARIO = os.environ.get("APP_USUARIO", "admin@iasis.com.br").strip().lower()
|
||||
SENHA = os.environ.get("APP_SENHA", "admin123")
|
||||
|
||||
# Assina o cookie de sessão. Sem SECRET_KEY no ambiente, sorteia uma por
|
||||
# processo: reiniciar o servidor derruba o login — a mesma vida útil que a
|
||||
# sessão da SD já tem (ver docstring), então nada se perde além do login.
|
||||
# Vale porque o gunicorn roda com -w 1 (ver Dockerfile); com mais workers,
|
||||
# cada um assinaria com uma chave diferente e SECRET_KEY passaria a ser
|
||||
# obrigatória.
|
||||
app.secret_key = os.environ.get("SECRET_KEY") or secrets.token_hex(32)
|
||||
|
||||
# Rotas que respondem sem login: a tela de entrada (`login`), o POST que a
|
||||
# submete (`entrar` — endpoint com outro nome, e sem ele o próprio login bate
|
||||
# no portão) e `static`, porque a tela de entrada precisa da folha de estilo.
|
||||
LIVRES = {"login", "entrar", "static"}
|
||||
|
||||
|
||||
def autenticado() -> bool:
|
||||
return session.get("autenticado") is True
|
||||
|
||||
|
||||
@app.before_request
|
||||
def exigir_login():
|
||||
"""O portão. Roda antes de qualquer rota — inclusive dos POSTs, então um
|
||||
POST direto em /exportar ou /salvar-sd sem cookie não chega ao handler."""
|
||||
if request.endpoint in LIVRES or autenticado():
|
||||
return None
|
||||
return redirect(url_for("login"))
|
||||
|
||||
|
||||
def credencial_confere(email: str, senha: str) -> bool:
|
||||
"""compare_digest nos dois campos: comparação de tempo constante, e sempre
|
||||
os dois — sair mais cedo no e-mail errado contaria qual dos dois falhou."""
|
||||
ok_email = hmac.compare_digest(email.strip().lower(), USUARIO)
|
||||
ok_senha = hmac.compare_digest(senha, SENHA)
|
||||
return ok_email and ok_senha
|
||||
|
||||
|
||||
@app.get("/login")
|
||||
def login():
|
||||
if autenticado():
|
||||
return redirect(url_for("inicio"))
|
||||
return render_template("login.html")
|
||||
|
||||
|
||||
@app.post("/login")
|
||||
def entrar():
|
||||
email = request.form.get("email", "")
|
||||
senha = request.form.get("senha", "")
|
||||
if not credencial_confere(email, senha):
|
||||
# Renderiza a própria tela com o erro, em vez de redirecionar: o
|
||||
# redirect exigiria carregar a mensagem em estado global, e a app já
|
||||
# tem uma sessão global só — duas abas se contaminariam.
|
||||
# Mensagem única para e-mail errado e senha errada, de propósito.
|
||||
return render_template("login.html", email=email,
|
||||
erro="Usuário ou senha inválidos."), 401
|
||||
session.clear()
|
||||
session["autenticado"] = True
|
||||
return redirect(url_for("inicio"))
|
||||
|
||||
|
||||
@app.post("/sair")
|
||||
def sair():
|
||||
"""Encerra a sessão do navegador. Não mexe na SD em memória: quem entrar
|
||||
depois continua de onde parou — é a mesma máquina, o mesmo processo."""
|
||||
session.clear()
|
||||
return redirect(url_for("login"))
|
||||
|
||||
|
||||
def canonico():
|
||||
return carregar_canonico()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user