first commit

This commit is contained in:
2026-02-18 20:34:33 -03:00
parent 2f819da943
commit 60f0dcaac4
50 changed files with 8099 additions and 1471 deletions

View File

@@ -0,0 +1,4 @@
from .routes import users_bp
from .model import db
__all__ = ["users_bp", "db"]

50
files/modules/users/db.py Normal file
View File

@@ -0,0 +1,50 @@
from pathlib import Path
import os
import re
import oracledb
import json
import base64
import hashlib
from datetime import datetime
import requests
import textwrap
import unicodedata
from typing import Optional
from collections import deque
from config_loader import load_config
def chunk_hash(text: str) -> str:
return hashlib.sha256(text.encode("utf-8")).hexdigest()
config = load_config()
# =========================
# Oracle Autonomous Configuration
# =========================
WALLET_PATH = config.wallet_path
DB_ALIAS = config.db_alias
USERNAME = config.username
PASSWORD = config.password
os.environ["TNS_ADMIN"] = WALLET_PATH
_pool = None
def get_pool():
global _pool
if _pool:
return _pool
_pool = oracledb.create_pool(
user=USERNAME,
password=PASSWORD,
dsn=DB_ALIAS,
config_dir=WALLET_PATH,
wallet_location=WALLET_PATH,
wallet_password=PASSWORD,
min=2,
max=8,
increment=1
)
return _pool

View File

@@ -0,0 +1,72 @@
import os
import smtplib
from email.message import EmailMessage
from flask import current_app
from config_loader import load_config
config = load_config()
API_BASE_URL = f"{config.app_base}:{config.service_port}"
def send_user_created_email(email, link, name=""):
"""
DEV -> return link only
PROD -> send real email
"""
if config.dev_mode == 1:
return link # 👈 só devolve o link
host = os.getenv("RFP_SMTP_HOST", "localhost")
port = int(os.getenv("RFP_SMTP_PORT", 25))
msg = EmailMessage()
msg["Subject"] = "Your account has been created"
msg["From"] = "noreply@rfp.local"
msg["To"] = email
msg.set_content(f"""
Hello {name or email},
Your account was created.
Set your password here:
{link}
""")
with smtplib.SMTP(host, port) as s:
s.send_message(msg)
return link
def send_completion_email(email, download_url, job_id):
"""
DEV -> return download link
PROD -> send real email
"""
if config.dev_mode == 1:
return download_url # 👈 só devolve o link no DEV
host = os.getenv("RFP_SMTP_HOST", "localhost")
port = int(os.getenv("RFP_SMTP_PORT", 25))
msg = EmailMessage()
msg["Subject"] = "Your RFP processing is complete"
msg["From"] = "noreply@rfp.local"
msg["To"] = email
msg.set_content(f"""
Hello,
Your RFP Excel processing has finished successfully.
Download your file here:
{download_url}
Job ID: {job_id}
""")
with smtplib.SMTP(host, port) as s:
s.send_message(msg)
return None

View File

@@ -0,0 +1,27 @@
from datetime import datetime
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
class User(db.Model):
__tablename__ = "users"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(120), nullable=False)
email = db.Column(db.String(160), unique=True, nullable=False, index=True)
role = db.Column(db.String(50), default="app") # app | admin
active = db.Column(db.Boolean, default=True)
password_hash = db.Column(db.String(255))
must_change_password = db.Column(db.Boolean, default=True)
reset_token = db.Column(db.String(255))
reset_expire = db.Column(db.DateTime)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
def __repr__(self):
return f"<User {self.email}>"

View File

@@ -0,0 +1,157 @@
from flask import Blueprint, render_template, request, redirect, url_for, flash
from modules.core.security import requires_admin_auth
from .service import (
signup_user,
list_users as svc_list_users,
create_user,
update_user,
delete_user as svc_delete_user,
get_user_by_token,
set_password_service
)
from .token_service import generate_token, expiration, is_expired
from .email_service import send_user_created_email
from config_loader import load_config
users_bp = Blueprint(
"users",
__name__,
template_folder="../../templates/users"
)
config = load_config()
# =========================
# LIST USERS (Oracle)
# =========================
@users_bp.route("/")
@requires_admin_auth
def list_users():
users = svc_list_users()
return render_template("list.html", users=users)
# =========================
# PUBLIC SIGNUP (Oracle)
# =========================
@users_bp.route("/signup", methods=["GET", "POST"])
def signup():
if request.method == "POST":
email = request.form.get("email", "").strip()
name = request.form.get("name", "").strip()
try:
link = signup_user(email=email, name=name)
except Exception as e:
flash(str(e), "danger")
return render_template("users/signup.html")
if link and config.dev_mode == 1:
flash(f"DEV MODE: password link → {link}", "success")
else:
flash("User created and email sent", "success")
return redirect(url_for("users.signup"))
return render_template("users/signup.html")
# =========================
# CREATE USER (Oracle)
# =========================
@users_bp.route("/new", methods=["GET", "POST"])
@requires_admin_auth
def new_user():
if request.method == "POST":
token = generate_token()
create_user(
name=request.form["name"],
email=request.form["email"],
role=request.form["role"],
active="active" in request.form,
token=token
)
link = url_for("users.set_password", token=token, _external=True)
dev_link = send_user_created_email(
request.form["email"],
link,
request.form["name"]
)
flash("User created and email sent", "success")
return redirect(url_for("users.list_users"))
return render_template("form.html", user=None)
# =========================
# EDIT USER (Oracle)
# =========================
@users_bp.route("/edit/<int:user_id>", methods=["GET", "POST"])
@requires_admin_auth
def edit_user(user_id):
if request.method == "POST":
update_user(
user_id=user_id,
name=request.form["name"],
email=request.form["email"],
role=request.form["role"],
active="active" in request.form
)
return redirect(url_for("users.list_users"))
# busca lista inteira e filtra (simples e funciona bem)
users = svc_list_users()
user = next((u for u in users if u["id"] == user_id), None)
return render_template("form.html", user=user)
# =========================
# DELETE USER (Oracle)
# =========================
@users_bp.route("/delete/<int:user_id>")
@requires_admin_auth
def delete_user(user_id):
svc_delete_user(user_id)
return redirect(url_for("users.list_users"))
# =========================
# SET PASSWORD (Oracle)
# =========================
@users_bp.route("/set-password/<token>", methods=["GET", "POST"])
def set_password(token):
user = get_user_by_token(token)
if not user or is_expired(user["expire"]):
return render_template("set_password.html", expired=True)
if request.method == "POST":
pwd = request.form["password"]
pwd2 = request.form["password2"]
if pwd != pwd2:
flash("Passwords do not match")
return render_template("set_password.html", expired=False)
set_password_service(user["id"], pwd)
flash("Password updated successfully")
return redirect("/")
return render_template("set_password.html", expired=False)

View File

@@ -0,0 +1,204 @@
#from .model import db, User
from .token_service import generate_token, expiration
from .email_service import send_user_created_email
from config_loader import load_config
from .db import get_pool
import bcrypt
from werkzeug.security import generate_password_hash, check_password_hash
config = load_config()
def authenticate_user(username: str, password: str):
print("LOGIN TRY:", username, password)
sql = """
SELECT password_hash
FROM app_users
WHERE email = :1 \
"""
pool = get_pool()
with pool.acquire() as conn:
with conn.cursor() as cur:
cur.execute(sql, [username])
row = cur.fetchone()
# print("ROW:", row)
if not row:
# print("USER NOT FOUND")
return False
stored_hash = row[0]
# print("HASH:", stored_hash)
ok = check_password_hash(stored_hash, password)
# print("MATCH:", ok)
return ok
def create_user(username: str, password: str):
hashed = bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode()
sql = """
INSERT INTO app_users (username, password_hash)
VALUES (:1, :2) \
"""
pool = get_pool()
with pool.acquire() as conn:
with conn.cursor() as cur:
cur.execute(sql, [username, hashed])
conn.commit()
def _default_name(email: str) -> str:
return (email or "").split("@")[0]
def signup_user(email: str, name: str = ""):
if not email:
raise ValueError("Email required")
email = email.lower().strip()
name = name or email.split("@")[0]
token = generate_token()
pool = get_pool()
sql_check = """
SELECT id
FROM app_users
WHERE email = :1 \
"""
sql_insert = """
INSERT INTO app_users
(name,email,user_role,active,reset_token,reset_expire,must_change_password)
VALUES (:1,:2,'user',1,:3,:4,1) \
"""
sql_update = """
UPDATE app_users
SET reset_token=:1,
reset_expire=:2,
must_change_password=1
WHERE email=:3 \
"""
with pool.acquire() as conn:
with conn.cursor() as cur:
cur.execute(sql_check, [email])
row = cur.fetchone()
if not row:
cur.execute(sql_insert, [name, email, token, expiration()])
else:
cur.execute(sql_update, [token, expiration(), email])
conn.commit()
link = f"{config.app_base}:{config.service_port}/admin/users/set-password/{token}"
dev_link = send_user_created_email(email, link, name)
return dev_link or link
def list_users():
sql = """
SELECT id, name, email, user_role, active
FROM app_users
ORDER BY name \
"""
pool = get_pool()
with pool.acquire() as conn:
with conn.cursor() as cur:
cur.execute(sql)
cols = [c[0].lower() for c in cur.description]
return [dict(zip(cols, r)) for r in cur.fetchall()]
def create_user(name, email, role, active, token):
sql = """
INSERT INTO app_users
(name,email,user_role,active,reset_token,reset_expire,must_change_password)
VALUES (:1,:2,:3,:4,:5,SYSTIMESTAMP + INTERVAL '1' DAY,1) \
"""
pool = get_pool()
with pool.acquire() as conn:
with conn.cursor() as cur:
cur.execute(sql, [name, email, role, active, token])
conn.commit()
def update_user(user_id, name, email, role, active):
sql = """
UPDATE app_users
SET name=:1, email=:2, user_role=:3, active=:4
WHERE id=:5 \
"""
pool = get_pool()
with pool.acquire() as conn:
with conn.cursor() as cur:
cur.execute(sql, [name, email, role, active, user_id])
conn.commit()
def delete_user(user_id):
sql = "DELETE FROM app_users WHERE id=:1"
pool = get_pool()
with pool.acquire() as conn:
with conn.cursor() as cur:
cur.execute(sql, [user_id])
conn.commit()
def get_user_by_token(token):
sql = """
SELECT id, reset_expire
FROM app_users
WHERE reset_token=:1 \
"""
pool = get_pool()
with pool.acquire() as conn:
with conn.cursor() as cur:
cur.execute(sql, [token])
row = cur.fetchone()
if not row:
return None
return {"id": row[0], "expire": row[1]}
def set_password_service(user_id, pwd):
hashed = generate_password_hash(pwd)
sql = """
UPDATE app_users
SET password_hash=:1,
must_change_password=0,
reset_token=NULL,
reset_expire=NULL
WHERE id=:2 \
"""
pool = get_pool()
with pool.acquire() as conn:
with conn.cursor() as cur:
cur.execute(sql, [hashed, user_id])
conn.commit()

View File

@@ -0,0 +1,14 @@
import secrets
from datetime import datetime, timedelta
def generate_token():
return secrets.token_urlsafe(48)
def expiration(hours=24):
return datetime.utcnow() + timedelta(hours=hours)
def is_expired(expire_dt):
return not expire_dt or expire_dt < datetime.utcnow()