From bae6b1ae14ca227d7730e0d55d56a3e576fd72ea Mon Sep 17 00:00:00 2001 From: LS Date: Thu, 27 Mar 2025 14:34:16 -0300 Subject: [PATCH] Login finalizado, admin funcionando corretamente e sendo gerado oQRcode na raiz do projeto --- Makefile | 9 +++- admin_qr.png | Bin 0 -> 1250 bytes app.py | 105 +++++++++++++++++++++++++++++++++++++----- create_admin.py | 48 +++++++++++++------ functions/database.py | 2 +- templates/base.html | 15 ++++++ 6 files changed, 153 insertions(+), 26 deletions(-) create mode 100644 admin_qr.png diff --git a/Makefile b/Makefile index bc6c28a..969466b 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,12 @@ install: pip install -r requirements.txt -run: +clean: + rm -rf ~/.local/share/controles/database.db + rm -f admin_qr.png + +run: clean python app.py + +reset-admin: clean + python create_admin.py diff --git a/admin_qr.png b/admin_qr.png new file mode 100644 index 0000000000000000000000000000000000000000..270fcde8af350adea1ab24e3c350f30932056c08 GIT binary patch literal 1250 zcmV<81ReW{P) zU25z`6ojkO0zdY^EWk@??;uOMcxTd5ynsN@qoGCRq5qCF7jm80=}BC|n6ae!kf3Nz z*ZHv{{`s;0_6ZO`6j4MGMHEp)5k(YHL=i=l17&md0I)mva$DeDZVRwob_d*+eoH-9 zkINT4izuReh!RMW8Q9ztxng%*L#`K#fZHwZM*>KaT)*U5L=ojfl)bN_?bCJv+>hjm zm{6ACvhD(PrT(gC5k-{0O^HRZdkG=1J(EDlC~NU{`8S_M6j6SDEawxEfZHw}4)Db0 zfbs+E3u4;uzVTphB+9p=c#;Vy`?8n-M84N3cMT-@_C&s3+I^AaJ>_$rMHEp!K)H1j z;rZS1_4(cLwS~VeaCmQ@E5WAB zE)-bBGJ`8T5tHwK0yd%u8AjPr+7)FQ#Td0*1Bu8C0KNya{(N=lS=tq4BBg8BS79>w zOnMG+kujH$25m@8h6*Wsy zlEq`!#$u0o?bSq?N$IY^*rv5BF?kj#cbzs-kFY}86=fEsue&uM7>X_GM03XMT1*>O zNV}rUq4W%|&n_kqxb3CJ#>I36fqel&Dp_F{(*@j|CxAS%!;kbvqI?&MiFd037TDqg z=H}og&!Z{UtzE6VqRgTgkE}@)Xvv74O*0vZ<2h+plxdWsN0yB`hnL$5a#~s<%4ZPe z0Vv0qfi)_q)^3&P>C+*LEbWRii*jn-Y5p#0Ert<7&{jSwW)Nj2<>-+e#A-aUo@*OR z?keqy@(`5L)q`q}m9q2V;9_^c?Wx8LrRx&A144%CcSV^QSUF))CQ^)ITNisXZ&%zEFx1NQQE#TbnkdsK2kXG}rsaOMW+a-i(93WD)$fWj zlVV~9Q)h0a3=`X$l$vTFMVU;od$l;C2l$@#9q~c#rRcSiSL?1Q6Dj9(!&#kilsB~8 zag8HFR-dpaGbx82_byE9IE-eX_N%mBnu1AbSCk19TLalWh0`#Rs@TyOoJPyLv@6Oi zO6z?He>lW+&1HCAOjjVjE!hD+WBc9Hl}zyMnt_l{1N9#1jYRn_6g#1542!NVnCHRT zt!cI~sX?_kqD-V5a}%TD+?#>BCd~%e3-TF6nMg5vwL{jo_X(H7AM4R!l5o1oL0j%zTD1bMN8bNRa{EPo1{QsJn=-zyJUM M07*qoM6N<$f{tigL;wH) literal 0 HcmV?d00001 diff --git a/app.py b/app.py index afce09a..29b8924 100644 --- a/app.py +++ b/app.py @@ -1,4 +1,4 @@ -from flask import Flask, request, render_template, redirect, url_for, flash, session +from flask import Flask, request, render_template, redirect, url_for, flash, session, jsonify from functions.database import ( Base, Militante, @@ -17,12 +17,14 @@ from functions.database import ( ) from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker -from datetime import datetime +from datetime import datetime, timedelta from flask_bootstrap import Bootstrap5 from routes.cota import cota_bp from functions.validations import validar_cpf from functools import wraps from pathlib import Path +from time import time +from create_admin import generate_qr_code app = Flask(__name__) app.secret_key = 'sua_chave_secreta_aqui' # Necessário para sessões do Flask @@ -41,6 +43,39 @@ def login_required(f): return f(*args, **kwargs) return decorated_function +# Decorator para verificar se a sessão expirou +def session_timeout(f): + @wraps(f) + def decorated_function(*args, **kwargs): + if 'user_id' not in session: + return redirect(url_for('login')) + + # Verificar se existe timestamp do último acesso + if 'last_activity' in session: + last_activity = datetime.fromtimestamp(session['last_activity']) + now = datetime.now() + + # Se passaram mais de 2 horas + if now - last_activity > timedelta(hours=2): + # Registrar o logout por timeout + try: + user = db_session.query(Usuario).get(session['user_id']) + if user: + user.ultimo_logout = datetime.now() + user.motivo_logout = "Timeout de sessão" + db_session.commit() + except Exception as e: + print(f"Erro ao registrar logout por timeout: {e}") + + session.clear() + flash('Sua sessão expirou. Por favor, faça login novamente.', 'warning') + return redirect(url_for('login')) + + # Atualizar timestamp de último acesso + session['last_activity'] = time() + return f(*args, **kwargs) + return decorated_function + # Rota raiz - redireciona para login se não estiver autenticado @app.route("/") def index(): @@ -85,6 +120,12 @@ def login(): print(f"Login bem sucedido para o usuário '{username}'") session['user_id'] = user.id session['is_admin'] = user.is_admin + session['last_activity'] = time() # Inicializar timestamp + + # Registrar horário do login + user.ultimo_login = datetime.now() + db_session.commit() + next_page = request.args.get('next') flash('Login realizado com sucesso!', 'success') return redirect(next_page or url_for('home')) @@ -101,6 +142,7 @@ def logout(): # Rota home (protegida) @app.route("/home") @login_required +@session_timeout def home(): """Página inicial do sistema""" try: @@ -137,6 +179,7 @@ def home(): # Rota para criar um novo militante @app.route("/militantes/novo", methods=["GET", "POST"]) @login_required +@session_timeout def novo_militante(): if request.method == "POST": cpf = request.form["cpf"] @@ -172,6 +215,7 @@ def novo_militante(): # Rota para listar militantes @app.route("/militantes") @login_required +@session_timeout def listar_militantes(): militantes = db_session.query(Militante).all() return render_template("listar_militantes.html", militantes=militantes) @@ -179,6 +223,7 @@ def listar_militantes(): # Rota para criar uma nova cota mensal @app.route("/cotas/novo", methods=["GET", "POST"]) @login_required +@session_timeout def nova_cota(): if request.method == "POST": cotas_mensais = CotaMensal( @@ -203,6 +248,7 @@ def nova_cota(): # Rota para listar cotas mensais @app.route("/cotas") @login_required +@session_timeout def listar_cotas(): cotas = db_session.query(CotaMensal).all() return render_template("listar_cotas.html", cotas=cotas) @@ -210,6 +256,7 @@ def listar_cotas(): # Rota para criar um novo pagamento @app.route("/pagamentos/novo", methods=["GET", "POST"]) @login_required +@session_timeout def novo_pagamento(): if request.method == "POST": pagamentos = Pagamento( @@ -234,6 +281,7 @@ def novo_pagamento(): # Rota para listar pagamentos @app.route("/pagamentos") @login_required +@session_timeout def listar_pagamentos(): pagamentos = db_session.query(Pagamento).all() return render_template("listar_pagamentos.html", pagamentos=pagamentos) @@ -241,6 +289,7 @@ def listar_pagamentos(): # Rota para criar um novo material vendido @app.route("/materiais/novo", methods=["GET", "POST"]) @login_required +@session_timeout def novo_material(): if request.method == "POST": materiais_vendidos = MaterialVendido( @@ -266,6 +315,7 @@ def novo_material(): # Rota para listar materiais vendidos @app.route("/materiais") @login_required +@session_timeout def listar_materiais(): materiais = db_session.query(MaterialVendido).all() return render_template("listar_materiais.html", materiais=materiais) @@ -273,6 +323,7 @@ def listar_materiais(): # Rota para criar uma nova venda de jornais avulsos @app.route("/jornais/novo", methods=["GET", "POST"]) @login_required +@session_timeout def nova_venda_jornal(): if request.method == "POST": vendas_jornais_avulsos = VendaJornalAvulso( @@ -297,6 +348,7 @@ def nova_venda_jornal(): # Rota para listar vendas de jornais avulsos @app.route("/jornais") @login_required +@session_timeout def listar_vendas_jornal(): vendas = db_session.query(VendaJornalAvulso).all() return render_template("listar_vendas_jornal.html", vendas=vendas) @@ -304,6 +356,7 @@ def listar_vendas_jornal(): # Rota para criar uma nova assinatura anual @app.route("/assinaturas/novo", methods=["GET", "POST"]) @login_required +@session_timeout def nova_assinatura(): if request.method == "POST": assinaturas_anuais = AssinaturaAnual( @@ -330,6 +383,7 @@ def nova_assinatura(): # Rota para listar assinaturas anuais @app.route("/assinaturas") @login_required +@session_timeout def listar_assinaturas(): assinaturas = db_session.query(AssinaturaAnual).all() return render_template("listar_assinaturas.html", assinaturas=assinaturas) @@ -337,6 +391,7 @@ def listar_assinaturas(): # Rota para criar um novo relatório de cotas mensais @app.route("/relatorios/cotas/novo", methods=["GET", "POST"]) @login_required +@session_timeout def novo_relatorio_cotas(): if request.method == "POST": relatorio_cotas_mensais = RelatorioCotasMensais( @@ -361,6 +416,7 @@ def novo_relatorio_cotas(): # Rota para listar relatórios de cotas mensais @app.route("/relatorios/cotas") @login_required +@session_timeout def listar_relatorios_cotas(): relatorios = db_session.query(RelatorioCotasMensais).all() return render_template("listar_relatorios_cotas.html", relatorios=relatorios) @@ -368,6 +424,7 @@ def listar_relatorios_cotas(): # Rota para criar um novo relatório de vendas de materiais @app.route("/relatorios/vendas/novo", methods=["GET", "POST"]) @login_required +@session_timeout def novo_relatorio_vendas(): if request.method == "POST": relatorio_vendas_materiais = RelatorioVendasMateriais( @@ -392,12 +449,14 @@ def novo_relatorio_vendas(): # Rota para listar relatórios de vendas de materiais @app.route("/relatorios/vendas") @login_required +@session_timeout def listar_relatorios_vendas(): relatorios = db_session.query(RelatorioVendasMateriais).all() return render_template("listar_relatorios_vendas.html", relatorios=relatorios) @app.route("/militantes/editar/", methods=["GET", "POST"]) @login_required +@session_timeout def editar_militante(id): militante = db_session.query(Militante).get(id) if not militante: @@ -469,17 +528,11 @@ def novo_usuario(): db_session.add(novo_usuario) db_session.commit() - # Gerar QR code para configuração do OTP + # Gerar QR code usando a função do create_admin.py qr_uri = novo_usuario.get_otp_uri() + qr_path = generate_qr_code(novo_usuario) + flash('Usuário criado com sucesso!', 'success') - - # Modificação alternativa para salvar no diretório atual - qr_path = Path('admin_qr.png') - img = qrcode.QRCode(version=1, box_size=10, border=5) - img.add_data(qr_uri) - img.make(fit=True) - img.save(str(qr_path)) - return render_template('mostrar_qr_code.html', qr_uri=qr_uri) except Exception as e: @@ -490,6 +543,30 @@ def novo_usuario(): return render_template('novo_usuario.html') +@app.route('/check_session') +def check_session(): + if 'last_activity' not in session: + return jsonify({'expired': True}) + + last_activity = datetime.fromtimestamp(session['last_activity']) + now = datetime.now() + + if now - last_activity > timedelta(hours=2): + # Registrar o logout por timeout + try: + user = db_session.query(Usuario).get(session.get('user_id')) + if user: + user.ultimo_logout = datetime.now() + user.motivo_logout = "Timeout de sessão" + db_session.commit() + except Exception as e: + print(f"Erro ao registrar logout por timeout: {e}") + + session.clear() + return jsonify({'expired': True}) + + return jsonify({'expired': False}) + def create_app(): app = Flask(__name__) # ... existing code ... @@ -501,4 +578,10 @@ def create_app(): # Iniciar o servidor Flask if __name__ == "__main__": + # Verificar se existe usuário admin e gerar QR code + admin = db_session.query(Usuario).filter_by(username="admin").first() + if admin: + print("\n=== Gerando QR Code para usuário admin existente ===") + generate_qr_code(admin) + app.run(debug=True) diff --git a/create_admin.py b/create_admin.py index 6544061..626221f 100644 --- a/create_admin.py +++ b/create_admin.py @@ -4,6 +4,36 @@ import os from pathlib import Path import pyotp +def generate_qr_code(user): + """ + Gera o QR code para um usuário específico + + Args: + user: Instância do modelo Usuario + + Returns: + Path: Caminho do arquivo QR code gerado + """ + import qrcode + + # Gerar QR Code apenas na raiz do projeto + qr_path = Path('admin_qr.png') + + # Remover arquivo antigo se existir + if qr_path.exists(): + os.remove(str(qr_path)) + + # Gerar e salvar QR Code + qr = qrcode.QRCode(version=1, box_size=10, border=5) + qr.add_data(user.get_otp_uri()) + qr.make(fit=True) + img = qr.make_image(fill_color="black", back_color="white") + img.save(str(qr_path)) + + print(f"\nQR Code gerado em: {os.path.abspath(qr_path)}") + + return qr_path + def create_admin_user(): try: # Inicializar o banco de dados @@ -17,6 +47,8 @@ def create_admin_user(): if admin: print("\n=== Usuário Admin Encontrado ===") + # Atualizar QR code mesmo se o usuário já existir + qr_path = generate_qr_code(admin) else: print("\n=== Criando Novo Usuário Admin ===") # Criar usuário admin com novo segredo OTP @@ -45,21 +77,11 @@ def create_admin_user(): totp = pyotp.TOTP(admin.otp_secret) otp_uri = totp.provisioning_uri( name=admin.username, - issuer_name="Sistema de Gestão" + issuer_name="Sistema de Controles" ) - # Configurar diretório para o QR Code - home = Path.home() - qr_dir = home / '.local' / 'share' / 'controles' / 'qrcodes' - qr_dir.mkdir(parents=True, exist_ok=True) - - # Gerar e salvar QR Code - qr_path = qr_dir / 'admin_qr.png' - qr = qrcode.QRCode(version=1, box_size=10, border=5) - qr.add_data(otp_uri) - qr.make(fit=True) - img = qr.make_image(fill_color="black", back_color="white") - img.save(str(qr_path)) + # Usar a função extraída para gerar o QR code + qr_path = generate_qr_code(admin) print("\n=== QR Code Gerado ===") print(f"QR Code salvo em: {qr_path}") diff --git a/functions/database.py b/functions/database.py index ebb9ce1..d3b16d4 100644 --- a/functions/database.py +++ b/functions/database.py @@ -235,7 +235,7 @@ class Usuario(Base): totp = pyotp.TOTP(self.otp_secret) return totp.provisioning_uri( name=self.username, - issuer_name="Sistema de Gestão" + issuer_name="Sistema de Controles" ) class Role(Base): diff --git a/templates/base.html b/templates/base.html index 0a4cf99..35dc81a 100644 --- a/templates/base.html +++ b/templates/base.html @@ -48,5 +48,20 @@ {{ bootstrap.load_js() }} + + {% if 'user_id' in session %} + + {% endif %} \ No newline at end of file