from sqlalchemy import create_engine, Column, Integer, String, Boolean, Numeric, Date, ForeignKey, DateTime, Text from sqlalchemy.orm import relationship, sessionmaker from sqlalchemy.ext.declarative import declarative_base from werkzeug.security import generate_password_hash, check_password_hash import pyotp import os from pathlib import Path from sqlalchemy.pool import NullPool from datetime import datetime, timedelta import secrets from flask_mail import Message from flask import url_for # Configurar caminho do banco de dados db_dir = Path.home() / '.local' / 'share' / 'controles' db_dir.mkdir(parents=True, exist_ok=True) db_path = db_dir / 'database.db' # Configurar engine com NullPool engine = create_engine( f'sqlite:///{db_path}', echo=True, poolclass=NullPool # Usar NullPool ao invés do pool padrão ) Base = declarative_base() SessionLocal = sessionmaker(bind=engine) def get_db_connection(): """ Retorna uma nova sessão do banco de dados """ try: return SessionLocal() finally: engine.dispose() def execute_query(query, params=None): """ Executa uma query usando SQLAlchemy """ session = get_db_connection() try: result = session.execute(query, params) session.commit() return result except Exception as e: session.rollback() raise e finally: session.close() class Celula(Base): __tablename__ = 'celulas' id = Column(Integer, primary_key=True, autoincrement=True) nome = Column(String(100), nullable=False) setor_id = Column(Integer, ForeignKey('setores.id')) cr_id = Column(Integer, ForeignKey('comites_regionais.id')) secretario = Column(Integer, ForeignKey('militantes.id')) responsavel_financas = Column(Integer, ForeignKey('militantes.id')) quadro_orientador = Column(String(255)) # Relacionamentos setor = relationship("Setor", back_populates="celulas") cr = relationship("ComiteRegional", back_populates="celulas") militantes = relationship("Militante", back_populates="celula", foreign_keys="[Militante.celula_id]") secretario_rel = relationship("Militante", foreign_keys=[secretario]) responsavel_financas_rel = relationship("Militante", foreign_keys=[responsavel_financas]) pagamentos = relationship("PagamentoCelula", back_populates="celula") class ComiteRegional(Base): __tablename__ = 'comites_regionais' id = Column(Integer, primary_key=True, autoincrement=True) nome = Column(String(100), nullable=False) responsavel_financas = Column(Integer, ForeignKey('militantes.id')) responsavel_formacao = Column(Integer, ForeignKey('militantes.id')) secretario_organizacao = Column(Integer, ForeignKey('militantes.id')) correspondente_jornal = Column(Integer, ForeignKey('militantes.id')) # Relacionamentos responsavel_financas_rel = relationship("Militante", foreign_keys=[responsavel_financas]) responsavel_formacao_rel = relationship("Militante", foreign_keys=[responsavel_formacao]) secretario_organizacao_rel = relationship("Militante", foreign_keys=[secretario_organizacao]) correspondente_jornal_rel = relationship("Militante", foreign_keys=[correspondente_jornal]) setores = relationship("Setor", back_populates="cr") celulas = relationship("Celula", back_populates="cr") class EmailMilitante(Base): __tablename__ = 'emails_militantes' id = Column(Integer, primary_key=True, autoincrement=True) militante_id = Column(Integer, ForeignKey('militantes.id')) endereco_email = Column(String(100)) militante = relationship("Militante", back_populates="emails") class Endereco(Base): __tablename__ = 'enderecos' id = Column(Integer, primary_key=True, autoincrement=True) estado = Column(String(2)) cidade = Column(String(50)) bairro = Column(String(50)) rua = Column(String(100)) numero = Column(String(10)) complemento = Column(String(50)) cep = Column(String(9)) militantes = relationship("Militante", back_populates="endereco") class RedeSocial(Base): __tablename__ = 'redes_sociais' id = Column(Integer, primary_key=True, autoincrement=True) militante_id = Column(Integer, ForeignKey('militantes.id')) tipo = Column(String(20)) # Instagram, TikTok, Discord, etc. identificador = Column(String(100)) militante = relationship("Militante", back_populates="redes_sociais") class Militante(Base): __tablename__ = 'militantes' id = Column(Integer, primary_key=True, autoincrement=True) nome = Column(String(100), nullable=False) cpf = Column(String(14), unique=True) # Novos campos básicos titulo_eleitoral = Column(String(20)) data_nascimento = Column(Date) data_entrada_oci = Column(Date) data_efetivacao_oci = Column(Date) # Campos de contato telefone1 = Column(String(15)) telefone2 = Column(String(15)) # Relacionamento para múltiplos emails emails = relationship("EmailMilitante", back_populates="militante") # Endereço endereco_id = Column(Integer, ForeignKey('enderecos.id')) endereco = relationship("Endereco", back_populates="militantes") # Redes sociais redes_sociais = relationship("RedeSocial", back_populates="militante") # Campos profissionais profissao = Column(String(100)) regime_trabalho = Column(String(50)) # CLT, Estatutário, etc. empresa = Column(String(100)) contratante = Column(String(100)) # Para terceirizados # Campos acadêmicos instituicao_ensino = Column(String(100)) tipo_instituicao = Column(String(20)) # Federal, Estadual, etc. # Campos sindicais sindicato = Column(String(100)) cargo_sindical = Column(String(50)) dirigente_sindical = Column(Boolean) central_sindical = Column(String(100)) # Responsável pelo cadastro registrado_por = Column(Integer, ForeignKey('militantes.id')) # Campos existentes celula_id = Column(Integer, ForeignKey('celulas.id')) responsabilidades = Column(Integer, default=0) otp_secret = Column(String(32)) temp_token = Column(String(64)) temp_token_expiry = Column(DateTime) # Relacionamentos existentes cotas_mensais = relationship("CotaMensal", back_populates="militante") pagamentos = relationship("Pagamento", back_populates="militante") materiais_vendidos = relationship("MaterialVendido", back_populates="militante") vendas_jornais = relationship("VendaJornalAvulso", back_populates="militante") assinaturas = relationship("AssinaturaAnual", back_populates="militante") celula = relationship("Celula", back_populates="militantes", foreign_keys=[celula_id]) # Constantes para responsabilidades SECRETARIO = 1 TESOUREIRO = 2 IMPRENSA = 4 MNS = 8 MPS = 16 JUVENTUDE = 32 @staticmethod def get_responsabilidades_list(): return [ (Militante.SECRETARIO, "Secretário"), (Militante.TESOUREIRO, "Tesoureiro"), (Militante.IMPRENSA, "Imprensa"), (Militante.MNS, "MNS"), (Militante.MPS, "MPS"), (Militante.JUVENTUDE, "Juventude") ] def set_responsabilidades(self, resp_list): """ Define as responsabilidades do militante resp_list: lista de inteiros representando as responsabilidades """ self.responsabilidades = sum(resp_list) def get_responsabilidades(self): """ Retorna lista de responsabilidades ativas """ resp = [] for valor, nome in self.get_responsabilidades_list(): if self.responsabilidades & valor: resp.append(nome) return resp def generate_temp_token(self): """ Gera um token temporário para acesso ao QR code """ self.temp_token = secrets.token_urlsafe(32) self.temp_token_expiry = datetime.now() + timedelta(hours=48) return self.temp_token def send_otp_email(self, mail): """ Envia email com link para QR code """ token = self.generate_temp_token() qr_url = url_for('get_qr_code', token=token, _external=True) msg = Message( 'Configuração de Autenticação em Duas Etapas', recipients=[self.email] ) msg.body = f""" Olá {self.nome}, Para configurar sua autenticação em duas etapas, acesse o link abaixo: {qr_url} Este link expirará em 48 horas. Instruções: 1. Instale um aplicativo autenticador (Google Authenticator, Microsoft Authenticator) 2. Acesse o link acima 3. Escaneie o QR code com o aplicativo 4. Use o código gerado para fazer login no sistema Atenciosamente, Sistema de Controles """ mail.send(msg) class CotaMensal(Base): __tablename__ = 'cotas_mensais' id = Column(Integer, primary_key=True, autoincrement=True) militante_id = Column(Integer, ForeignKey('militantes.id')) valor_antigo = Column(Numeric(10, 2), nullable=False) valor_novo = Column(Numeric(10, 2), nullable=False) data_alteracao = Column(Date, nullable=False) militante = relationship("Militante", back_populates="cotas_mensais") class TipoPagamento(Base): __tablename__ = 'tipos_pagamento' id = Column(Integer, primary_key=True, autoincrement=True) descricao = Column(String(100), nullable=False) class Pagamento(Base): __tablename__ = 'pagamentos' id = Column(Integer, primary_key=True, autoincrement=True) militante_id = Column(Integer, ForeignKey('militantes.id')) tipo_pagamento = Column(String(50)) # Cota, Jornal, Assinatura, etc. mes_referencia = Column(Date) numero_jornal = Column(String(20)) numero_inicial_assinatura = Column(String(20)) numero_final_assinatura = Column(String(20)) campanha_financeira = Column(String(50)) valor = Column(Numeric(10, 2), nullable=False) data_pagamento = Column(Date, nullable=False) militante = relationship("Militante", back_populates="pagamentos") transacoes_pix = relationship("TransacaoPIX", back_populates="pagamento") class TipoMaterial(Base): __tablename__ = 'tipos_materiais' id = Column(Integer, primary_key=True, autoincrement=True) descricao = Column(String(100), nullable=False) materiais_vendidos = relationship("MaterialVendido", back_populates="tipo_material") assinaturas = relationship("AssinaturaAnual", back_populates="tipo_material") class MaterialVendido(Base): __tablename__ = 'materiais_vendidos' id = Column(Integer, primary_key=True, autoincrement=True) militante_id = Column(Integer, ForeignKey('militantes.id')) tipo_material_id = Column(Integer, ForeignKey('tipos_materiais.id')) descricao = Column(String(255), nullable=False) valor = Column(Numeric(10, 2), nullable=False) data_venda = Column(Date, nullable=False) militante = relationship("Militante", back_populates="materiais_vendidos") tipo_material = relationship("TipoMaterial", back_populates="materiais_vendidos") class VendaJornalAvulso(Base): __tablename__ = 'vendas_jornais_avulsos' id = Column(Integer, primary_key=True, autoincrement=True) militante_id = Column(Integer, ForeignKey('militantes.id')) quantidade = Column(Integer, nullable=False) valor_total = Column(Numeric(10, 2), nullable=False) data_venda = Column(Date, nullable=False) militante = relationship("Militante", back_populates="vendas_jornais") class AssinaturaAnual(Base): __tablename__ = 'assinaturas_anuais' id = Column(Integer, primary_key=True, autoincrement=True) militante_id = Column(Integer, ForeignKey('militantes.id')) tipo_material_id = Column(Integer, ForeignKey('tipos_materiais.id')) quantidade = Column(Integer, nullable=False) valor_total = Column(Numeric(10, 2), nullable=False) data_inicio = Column(Date, nullable=False) data_fim = Column(Date, nullable=False) militante = relationship("Militante", back_populates="assinaturas") tipo_material = relationship("TipoMaterial", back_populates="assinaturas") class Setor(Base): __tablename__ = 'setores' id = Column(Integer, primary_key=True) nome = Column(String(100), nullable=False) cr_id = Column(Integer, ForeignKey('comites_regionais.id')) responsavel = Column(Integer, ForeignKey('militantes.id')) responsavel_financas = Column(Integer, ForeignKey('militantes.id')) # Relacionamentos cr = relationship("ComiteRegional", back_populates="setores") responsavel_rel = relationship("Militante", foreign_keys=[responsavel]) responsavel_financas_rel = relationship("Militante", foreign_keys=[responsavel_financas]) usuarios = relationship("Usuario", back_populates="setor") celulas = relationship("Celula", back_populates="setor") relatorios_cotas = relationship("RelatorioCotasMensais", back_populates="setor") relatorios_vendas = relationship("RelatorioVendasMateriais", back_populates="setor") class ComiteCentral(Base): __tablename__ = 'comites_centrais' id = Column(Integer, primary_key=True, autoincrement=True) nome = Column(String(100), nullable=False) relatorios_cotas = relationship("RelatorioCotasMensais", back_populates="comite") relatorios_vendas = relationship("RelatorioVendasMateriais", back_populates="comite") class RelatorioCotasMensais(Base): __tablename__ = 'relatorio_cotas_mensais' id = Column(Integer, primary_key=True, autoincrement=True) setor_id = Column(Integer, ForeignKey('setores.id')) comite_id = Column(Integer, ForeignKey('comites_centrais.id')) total_cotas = Column(Numeric(10, 2), nullable=False) data_relatorio = Column(Date, nullable=False) setor = relationship("Setor", back_populates="relatorios_cotas") comite = relationship("ComiteCentral", back_populates="relatorios_cotas") class RelatorioVendasMateriais(Base): __tablename__ = 'relatorio_vendas_materiais' id = Column(Integer, primary_key=True, autoincrement=True) setor_id = Column(Integer, ForeignKey('setores.id')) comite_id = Column(Integer, ForeignKey('comites_centrais.id')) total_vendas = Column(Numeric(10, 2), nullable=False) data_relatorio = Column(Date, nullable=False) setor = relationship("Setor", back_populates="relatorios_vendas") comite = relationship("ComiteCentral", back_populates="relatorios_vendas") class Usuario(Base): __tablename__ = 'usuarios' id = Column(Integer, primary_key=True, autoincrement=True) username = Column(String(50), unique=True, nullable=False) password_hash = Column(String(255), nullable=False) email = Column(String(100), unique=True, nullable=False) otp_secret = Column(String(32)) role_id = Column(Integer, ForeignKey('roles.id')) setor_id = Column(Integer, ForeignKey('setores.id')) ativo = Column(Boolean, default=True) is_admin = Column(Boolean, default=False) ultimo_login = Column(DateTime) ultimo_logout = Column(DateTime) motivo_logout = Column(String(100)) cr_id = Column(Integer, ForeignKey('comites_regionais.id')) celula_id = Column(Integer, ForeignKey('celulas.id')) role = relationship("Role", back_populates="usuarios") setor = relationship("Setor", back_populates="usuarios") celula = relationship("Celula") cr = relationship("ComiteRegional") def __init__(self, username, password, is_admin=False): self.username = username self.password_hash = generate_password_hash(password) self.is_admin = is_admin self.otp_secret = pyotp.random_base32() # Gerar segredo OTP na criação self.ativo = True def check_password(self, password): return check_password_hash(self.password_hash, password) def verify_otp(self, otp_code): """Verifica se o código OTP fornecido é válido""" if not self.otp_secret: print(f"Erro: Usuário {self.username} não tem segredo OTP configurado") return False totp = pyotp.TOTP(self.otp_secret) is_valid = totp.verify(otp_code) print(f"Verificando OTP para {self.username}") print(f"Segredo: {self.otp_secret}") print(f"Código fornecido: {otp_code}") print(f"Resultado: {'válido' if is_valid else 'inválido'}") return is_valid def get_otp_uri(self): """Gera a URI para o QR code do OTP""" if not self.otp_secret: self.otp_secret = pyotp.random_base32() totp = pyotp.TOTP(self.otp_secret) return totp.provisioning_uri( name=self.username, issuer_name="Sistema de Controles" ) class Role(Base): __tablename__ = 'roles' id = Column(Integer, primary_key=True, autoincrement=True) nome = Column(String(50), unique=True, nullable=False) nivel = Column(Integer, nullable=False) # Nível hierárquico (1: admin, 2: coordenador, 3: militante) usuarios = relationship("Usuario", back_populates="role") permissoes = relationship("RolePermissao", back_populates="role") class Permissao(Base): __tablename__ = 'permissoes' id = Column(Integer, primary_key=True, autoincrement=True) nome = Column(String(50), unique=True, nullable=False) descricao = Column(String(255)) roles = relationship("RolePermissao", back_populates="permissao") class RolePermissao(Base): __tablename__ = 'roles_permissoes' role_id = Column(Integer, ForeignKey('roles.id'), primary_key=True) permissao_id = Column(Integer, ForeignKey('permissoes.id'), primary_key=True) role = relationship("Role", back_populates="permissoes") permissao = relationship("Permissao", back_populates="roles") class PagamentoCelula(Base): __tablename__ = 'pagamentos_celula' id = Column(Integer, primary_key=True, autoincrement=True) celula_id = Column(Integer, ForeignKey('celulas.id')) data = Column(Date) valor = Column(Numeric(10, 2)) metodo_pagamento = Column(String(20)) # PIX, Dinheiro, etc. codigo_pix = Column(String(100)) descricao = Column(String(255)) registrado_por = Column(Integer, ForeignKey('militantes.id')) celula = relationship("Celula", back_populates="pagamentos") registrado_por_rel = relationship("Militante", foreign_keys=[registrado_por]) class Atividade(Base): __tablename__ = 'atividades' id = Column(Integer, primary_key=True, autoincrement=True) descricao = Column(String(255)) data = Column(Date) responsavel1 = Column(Integer, ForeignKey('militantes.id')) responsavel2 = Column(Integer, ForeignKey('militantes.id')) responsavel1_rel = relationship("Militante", foreign_keys=[responsavel1]) responsavel2_rel = relationship("Militante", foreign_keys=[responsavel2]) materiais = relationship("MaterialAtividade", back_populates="atividade") class MaterialAtividade(Base): __tablename__ = 'materiais_atividades' id = Column(Integer, primary_key=True, autoincrement=True) atividade_id = Column(Integer, ForeignKey('atividades.id')) tipo = Column(String(20)) # Jornal, Revista, etc. quantidade = Column(Integer) detalhes = Column(String(255)) atividade = relationship("Atividade", back_populates="materiais") class Relatorio(Base): __tablename__ = 'relatorios' id = Column(Integer, primary_key=True, autoincrement=True) tipo = Column(String(50)) # Semanal, Quinzenal, Mensal periodo_inicio = Column(Date) periodo_fim = Column(Date) gerado_por = Column(Integer, ForeignKey('militantes.id')) conteudo = Column(Text) # Relacionamento hierárquico celula_id = Column(Integer, ForeignKey('celulas.id')) setor_id = Column(Integer, ForeignKey('setores.id')) cr_id = Column(Integer, ForeignKey('comites_regionais.id')) gerado_por_rel = relationship("Militante", foreign_keys=[gerado_por]) celula = relationship("Celula", foreign_keys=[celula_id]) setor = relationship("Setor", foreign_keys=[setor_id]) cr = relationship("ComiteRegional", foreign_keys=[cr_id]) class TransacaoPIX(Base): __tablename__ = 'transacoes_pix' id = Column(Integer, primary_key=True, autoincrement=True) chave_pix = Column(String(100)) valor = Column(Numeric(10, 2)) data_geracao = Column(DateTime) data_pagamento = Column(DateTime) status = Column(String(20)) # Pendente, Pago, Expirado qr_code = Column(Text) pagamento_id = Column(Integer, ForeignKey('pagamentos.id')) pagamento = relationship("Pagamento", back_populates="transacoes_pix") # Remover o banco de dados existente (se existir) if os.path.exists(db_path): os.remove(db_path) def init_database(): """Inicializa o banco de dados com dados básicos""" print("Inicializando banco de dados...") # Criar todas as tabelas Base.metadata.create_all(engine) session = SessionLocal() try: # Verificar se já existe um admin admin = session.query(Usuario).filter_by(username="admin").first() if not admin: print("Criando role de administrador...") # Criar role de admin admin_role = session.query(Role).filter_by(nome="Administrador").first() if not admin_role: admin_role = Role(nome="Administrador", nivel=1) session.add(admin_role) session.commit() print("Criando usuário admin...") # Criar usuário admin admin = Usuario( username="admin", password="admin123", is_admin=True ) admin.email = "admin@example.com" admin.role_id = admin_role.id session.add(admin) session.commit() print("=== Usuário Admin Criado ===") print(f"Username: admin") print(f"Senha: admin123") print(f"Email: {admin.email}") print(f"OTP Secret: {admin.otp_secret}") else: print("Usuário admin já existe") except Exception as e: print(f"Erro na inicialização do banco: {e}") session.rollback() raise finally: session.close() # Inicializar o banco de dados automaticamente quando o módulo for importado init_database() # Executar a criação dos dados iniciais if __name__ == "__main__": init_database()